﻿using CyberConnect.Insteon.Messaging;
using System;
using System.IO.Ports;

[assembly: CLSCompliant(true)]

namespace CyberConnect.Insteon
{
    public class PlmLinc : IDisposable
    {
        private bool IsDisposed = false;
        private readonly bool EndTransactions;
        private static readonly Object SyncRoot = new Object();
        private readonly SerialPort InsteonSignal;
        private PlmConfiguration _CurrentConfiguration;

        public PlmConfiguration CurrentConfiguration { get { return _CurrentConfiguration; } }

        public event IncomingMessageEventHandler IncomingMessageReceived;

        public event ButtonReportEventHandler ButtonReportReceived;

        public PlmLinc(string port)
            : this(port, false) { }

        public PlmLinc(string port, bool endTransactions)
        {
            this.EndTransactions = endTransactions;
            this.InsteonSignal = new SerialPort(port, 19200, Parity.None, 8, StopBits.One);

            this._CurrentConfiguration = this.GetConfiguration();

            if (this.EndTransactions)
                this.BeginTransaction();

            InsteonSignal.ReadTimeout = 2000;
            this.InsteonSignal.ReceivedBytesThreshold = 2;
            InsteonSignal.DataReceived += InsteonSignal_DataReceived;
        }

        public void UpdateConfiguration(PlmConfiguration configuration)
        {
            if (configuration == null) { throw new ArgumentNullException("configuration"); }

            byte[] configurationBytes = this.SendAndReceiveEcho(configuration.ToBytes(), 4);

            if (configurationBytes[configurationBytes.Length - 1] != 0x06)
                throw new PlmEchoNakException("Failed to set configuration from PLM on " + InsteonSignal.PortName + ".");

            this._CurrentConfiguration = new PlmConfiguration(configurationBytes[2]);
        }

        #region DeviceMessage

        public DeviceMessageIncoming SendReceiveDeviceMessage(DeviceMessage message, int timeout)
        {
            lock (SyncRoot)
            {
                try
                {
                    this.BeginTransaction();

                    this.SendDeviceMessageReceiveEcho(message, false);

                    this.ResetTransaction();

                    int count = 0;
                    while (InsteonSignal.BytesToRead < 11 && count < timeout)
                    {
                        count += 250;
                        System.Threading.Thread.Sleep(250);
                    }

                    return DeviceMessageIncoming.FromBytes(this.GetInboundMessageBytes(this.GetInboundPlmCommand()));
                }
                finally
                {
                    this.EndTransaction();
                }
            }
        }

        public DeviceMessageEcho SendDeviceMessageReceiveEcho(DeviceMessage message)
        {
            return this.SendDeviceMessageReceiveEcho(message, true);
        }

        public DeviceMessageEcho SendDeviceMessageReceiveEcho(DeviceMessage message, bool useTransactions)
        {
            if (message == null) { throw new ArgumentNullException("message"); }

            DeviceMessageEcho Echo;

            lock (SyncRoot)
            {
                try
                {
                    byte[] ModemEcho = useTransactions ? this.SendAndReceiveEcho(message.ToBytes(), 9) : this.SendAndReceiveEchoNoTransaction(message.ToBytes(), 9);

                    Echo = new DeviceMessageEcho(new DeviceMessage(new DeviceAddress(ModemEcho, 2), (MessageConfiguration)ModemEcho[5], new MessageCommand(ModemEcho[6], ModemEcho[7])), (EchoState)ModemEcho[8]);
                }
                catch (PlmEchoNakException)
                {
                    Echo = new DeviceMessageEcho(message, EchoState.Nak);
                }
            }

            return Echo;
        }

        public void SendDeviceMessage(DeviceMessage message)
        {
            System.Threading.ThreadPool.QueueUserWorkItem(_ => this.SendDeviceMessageReceiveEcho(message));
        }

        public void PingDevice(DeviceAddress address)
        {
            this.SendDeviceMessage(new DeviceMessage(address, (MessageConfiguration)0x0F, (MessageCommand)0x1000));
        }

        #endregion DeviceMessage

        #region MessagingEvents

        private void InsteonSignal_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            System.Diagnostics.Debug.Assert(e.EventType != SerialData.Eof);

            new System.Threading.Thread(WorkDataReceived).Start();
        }

        private void WorkDataReceived()
        {
            lock (SyncRoot)
            {
                if (InsteonSignal.BytesToRead >= 2)
                {
                    PlmCommand InboundCommand = this.GetInboundPlmCommand();

                    this.WorkDataReceived(InboundCommand);
                }
            }
        }

        private void WorkDataReceived(PlmCommand inboundCommand)
        {
            lock (SyncRoot)
            {
                if (InsteonSignal.BytesToRead >= 9)
                {
                    switch (inboundCommand)
                    {
                        case PlmCommand.StandardMessageReceived:
                        case PlmCommand.ExtendedMessageReceived:
                            if (this.IncomingMessageReceived != null)
                                this.IncomingMessageReceived(this, new DeviceMessageEventArgs(DeviceMessageIncoming.FromBytes(this.GetInboundMessageBytes(inboundCommand))));
                            break;

                        case PlmCommand.ButtonEventReport:
                            if (this.ButtonReportReceived != null)
                                this.ButtonReportReceived(this, new ButtonReportEventArgs((ButtonReport)InsteonSignal.ReadByte()));
                            break;

                        default:
                            break;
                    }
                }

                if (InsteonSignal.BytesToRead >= 2)
                    WorkDataReceived();
                else
                    this.ResetTransaction();
            }
        }

        private PlmCommand GetInboundPlmCommand()
        {
            byte[] buffer = new byte[2];

            try
            {
                InsteonSignal.Read(buffer, 0, 1);

                if (buffer[0] != 2)
                {
                    return GetInboundPlmCommand();

                    throw new TimeoutException("MessageOverlayOccured");
                }

                InsteonSignal.Read(buffer, 1, 1);
            }
            catch (TimeoutException)
            {
                buffer[1] = (byte)PlmCommand.None;
            }

            return (PlmCommand)buffer[1];
        }

        private byte[] GetInboundMessageBytes(PlmCommand command)
        {
            byte[] messageBytes = new byte[DeviceMessage.ReceivedMessageLength(command == PlmCommand.ExtendedMessageReceived) - 2];

            for (int i = 0; i < (messageBytes.Length); i++)
            {
                InsteonSignal.Read(messageBytes, i, 1);
            }

            return messageBytes;
        }

        #endregion MessagingEvents

        #region Internal Methods

        private PlmConfiguration GetConfiguration()
        {
            byte[] echoBytes;

            try
            {
                echoBytes = this.SendAndReceiveEcho(PlmConfiguration.GetQueryBytes(), 6);

                if (echoBytes[5] != (byte)EchoState.Ack)
                    throw new PlmEchoNakException("Failed to get configuration from PLM on " + InsteonSignal.PortName + ".");
            }
            catch (Exception ex)
            {
                throw new PlmEchoNakException("Failed to get configuration from PLM on " + InsteonSignal.PortName + ".", ex);
            }

            return new PlmConfiguration(echoBytes[1]);
        }

        /// <summary>
        /// Controls the PLM's LED. Requires DisableAutoLed set to true in the active PlmConfiguration.
        /// </summary>
        /// <param name="state">The state of the LED where true is on.</param>
        /// <returns>true if configuration was correct and message was processed; otherwise false.</returns>
        public bool Led(bool state)
        {
            byte[] ModemEcho;

            ModemEcho = this.SendAndReceiveEcho(new LedMessage(state).ToBytes(), 3);

            return new LedMessage(ModemEcho[1], (EchoState)ModemEcho[2]).IsValidEcho;
        }

        private byte[] SendAndReceiveEcho(byte[] message, int echoLength)
        {
            byte[] ModemEcho;

            lock (SyncRoot)
            {
                try
                {
                    this.BeginTransaction();

                    if (InsteonSignal.BytesToRead > 0)
                        WorkDataReceived();

                    this.ResetTransaction();

                    ModemEcho = SendAndReceiveEchoNoTransaction(message, echoLength);
                }

                finally
                {
                    this.EndTransaction();
                }
            }

            return ModemEcho;
        }

        private byte[] SendAndReceiveEchoNoTransaction(byte[] message, int echoLength)
        {
            byte[] ModemEcho = new byte[echoLength];

            System.Diagnostics.Debug.Assert(InsteonSignal.BytesToRead == 0);

            for (byte i = 0; i < message.Length; i++)
            {
                InsteonSignal.Write(message, i, 1);
                InsteonSignal.Read(ModemEcho, i, 1);

                if (message[i] != ModemEcho[i])
                {
                    if ((i == 1 && (byte)PlmCommand.StandardMessageReceived == ModemEcho[1]))
                        WorkDataReceived(PlmCommand.StandardMessageReceived);
                    else if (ModemEcho[i] == 0x02)
                        WorkDataReceived((PlmCommand)InsteonSignal.ReadByte());

                    throw new PlmEchoNakException();
                }
            }

            for (int i = message.Length; i < echoLength; i++)
            {
                InsteonSignal.Read(ModemEcho, i, 1);
            }

            return ModemEcho;
        }

        private void BeginTransaction()
        {
            if (!InsteonSignal.IsOpen)
                InsteonSignal.Open();
        }

        private void ResetTransaction()
        {
            InsteonSignal.DiscardInBuffer();
            InsteonSignal.DiscardOutBuffer();
        }

        private void EndTransaction()
        {
            if (this.EndTransactions)
                if (InsteonSignal.IsOpen)
                    InsteonSignal.Close();
        }

        #endregion Internal Methods

        #region IDisposable Pattern

        private void Dispose(bool disposing)
        {
            if (!IsDisposed)
            {
                if (disposing)
                {
                    InsteonSignal.Dispose();
                }

                this.IsDisposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);

            GC.SuppressFinalize(this);
        }

        ~PlmLinc()
        {
            this.Dispose(false);
        }

        #endregion IDisposable Pattern
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1003:UseGenericEventHandlerInstances")]
    public delegate void IncomingMessageEventHandler(object sender, DeviceMessageEventArgs e);

    public enum EchoState
    {
        None = 0x00,

        Ack = 0x06,

        Nak = 0x15
    }
}